在 Dart ,Flutter 的程式設計中,控制流程和迴圈同樣是非常基本且重要的概念,Dart 的控制流程語法基本上和其他語言非常類似,當然也存在一些獨特之處。
冷知識:Cupertino 開頭例如
CupertinoButton
為 iOS 風格逐漸組件,Cupertino 是蘋果總部所在的城市。
if/else
用於根據條件執行不同的程式碼區塊。Dart 的 if
語句可以不使用 {}
,尤其在 Flutter 的 Widget 定義非常實用。
Widget build(BuildContext context) {
return Column(
children: [
Text('持續顯示'),
if (condition)
Text('符合條件顯示'),
Text('其他'),
],
);
}
如果 if
語句的表達式結果是 true
則會執行後續的表達式
int a = 1;
if (a == 1) {
// 多個語句
}
if (a == 2) print(2);
// 不使用 {} 只能接受一個語句
需要注意的是, Dart 跟 JavaScript 不一樣,它沒有 Truthy 和 Falsy 的概念。也就是所有條件式必須明確為布林值 true
或 false
:
String s = 'true';
if (s) { // 錯誤:條件必須是布林值
}
在像是 JavaScript 的語言,兩個變數相等可以使用 ==
或 ===
,前者會嘗試將兩者轉換為相同型別,然後檢查值是否相等,後者會檢查型別和值是否相等。例如在 JavaScript "7" == 7
是 true
,但 "7" === 7
則是 false
。因為轉型的設計容易造成初階開發者的困惑,因此 Dart 並不會自動為我們轉型。雖然在撰寫時會感覺好像很麻煩,沒那麼方便,但長期對程式的品質而言比較健康。
while
和 do-while
迴圈用於重複執行程式碼,會在條件式為 true
的時候重複某段特定的程式。false
的時候終止。
do-while
和 while
的差異在於是在程式碼結束才判斷條件或是在迴圈的一開始就判斷:
int counter = 0;
while (counter < 2) {
print(counter);
counter++;
}
// 0, 1
do {
print(counter);
counter++;
} while (counter < 2);
// 2
for 迴圈提供了一種簡潔的方式來重複執行程式碼。它的基本結構如下:
for (初始化; 條件; 遞增/遞減) {
//
}
for (int i = 0; i < 5; i++) {
print(i);
}
break
和continue
關鍵字用於控制迴圈的執行流程。使用 break
可以讓我們自己跳出這個迴圈,continue
則是進入下一次迴圈:
int counter = 0;
while (counter < 10) {
counter++;
if (counter == 4) {
break;
} else (counter == 2) {
continue;
}
print(counter);
}
// 1, 3
switch
語句可以根據變數的值選擇分支執行程式碼。很類似 if/else 結構,但如果你希望一組特定值在分支執行,並保證每個值都有對應的分支。
這些特定的值,也叫 enum
列舉。switch
傳入一變數,然後為可能的值定義程式碼分支。
String location = ...;
switch (location) {
case 'Whitby':
//
break;
case 'Saltburn';
//
break;
default:
//
}
關於 switch
還有一些重點:
break
,swtich
語句會繼續執行下一 case
case
則執行 default
,default
是可選的。在Dart 3.0中,switch
語句支援了更複雜的模式匹配:
var pair = (1, 'one');
switch (pair) {
case (int x, String y) when x > 0:
print('正數: $y');
case (0, String y):
print('零: $y');
default:
print('其他情況');
}
詳細的教學可以參考官方介紹。
函式和方法都是將特定任務包含在其中的程式碼片段,語法上是相同的。
通常函式回傳的型別可以忽略不宣告,因為通常你回傳的值,Dart 都可以解析出型別,如果沒有 return
則假設回傳一個 dynamic
型別,如果是真的不用回傳則宣告 void
void main() {
String str = getStr();
print(getStr());
}
String getStr() {
return "Hi";
}
// 可以省略,回傳型別為 dynamic
getNext() {
return "Next one";
}
在這個範例中 getStr
為最高層級函式,換句話說,雖然 Dart 是物件導向語言,但不一定要用類別 class 封裝函式。
Parameters(參數) v.s Arguments(引數)
簡單說參數可理解為函式定義時列出來的變數佔位符,而引數則是呼叫提供的具體值。
void greet(String msg) { // msg 參數 print(msg); } String content = 'Hey'; greet(content); // content 是引數 greet("Hi"); // Hi 是引數
所謂參數指的是 函式語法格式中的項目 這個項目是用來宣告輸入資料的型別和名稱,就如同上面
msg
。而引數就是我們呼叫函式時傳入的資料,當我們呼叫函式時傳入引數的型別必須要符合參數宣告的型別。
一個函式可以有兩種類型的參數;必須和可選。此外,參數還可以具名,而不依賴順序位置,這讓程式碼更具可讀性。在 Flutter,Widget 包含很多可選參數,因此確定那些引數適用於參數對於理解程式碼非常重要。
一個參數的型別不是一定要宣告的;在這種情況下,參數會先假設為 dynamic
型別。但為了程式碼的可讀性和可維護性,建議還是加入型別宣告。
最簡單的函式定義就是使用按順序參數的方式來宣告。這是大部分語言常見的用法,你大概率已經很熟悉這種宣告方式。參數依照順序排列,而在呼叫函式的時候引數也是一樣按照相同順序如下範例:
sayHappyBirthday(String name, int age) {
return "$name is ${age.toString()} years old.";
}
// 呼叫
sayHappyBirthday("andyyou", 37);
有時候並不是全部的參數都一定強制要傳入,因此我們可以宣告可選參數。可選參數使用 []
來宣告。要注意的是可選參數一定要在必填參數後面:
sayHappyBirthday(String name, [int? age]) {
}
如果我們不傳入 age
那麼 age
將會是 null
也因此宣告時需要使用 int?
,當然如果你不希望允許為 null
那麼可以設定預設值
sayHappyBirthday(String name, [int age = 18]) {
}
讓我們重新來理解這個部分,[]
只決定了呼叫時參數能否被省略。一旦你宣告了,表示函式內部還是會用到這個參數 age
,也就是說當省略傳入時,不是 null
就一定是預設值取決於你的設計需求。換句話說也就是;不會有 int age
卻沒有預設值的情況。
具名參數使用 {}
語法定義。這些定義須在必填參數之後。跟可選位置參數一樣可以使用預設值。至於必填參數都已經是必填了,設定預設值永遠也無法使用。
sayHappyBirthday(String name, { int age = 7 }) {
}
呼叫時,具名參數須指定名稱(這個就是我們在 Flutter 使用 Widget 時會經常遇到的使用方式)
sayHappyBirthday("Laura", age: 21);
預設具名參數是可選的;呼叫時不一定要包含其引數,也就是預設具名參數要嘛是可以為空,要嘛是有預設值。但除此之外還可以使用 required
改成必填。
sayHappyBirthday(String name, { required int age }) {
}
函式參數總結:
greeting(String name, int age)
null
或者預設值。範例 greeting(String name, [int? age])
{}
語法,預設為可選,可選時呼叫可省略,值一樣為 null
或預設值,還可以使用 required
變更為必填 greeting(String name, { required int age })
Dart 3 之後新增了新的功能 Record
。這是新的資料型別,類似集合,讓我們可以在一個物件中存放一物件集合。
它可以混合位置參數和具名參數,Record 的語法類似函式參數:
(type1, type2, {<type3> name})
舉例來說如果你希望宣告一個 Record 型別的變數可以如下
(String, int) variables;
variables = ("andyyou", 20);
具名的用法也很類似函式參數:
({ String place, int distance }) point;
point = (place: 'Taiwan', distance: 200);
Record 的強大之處在於可以讓你支援函式包含多個回傳值,就跟我們可以為函式定義多個參數一樣。
舉例來說,假設你希望一個比賽的賽況資料區分為隊名和其分數,在支援 Record 型別之前,我們通常需要定義一個專用的類別 Class ,有了 Record 之後我們可以如下
(String, int, String, int) getScore() {
return ("Team 1", 4, "Team 2", 10);
}
然後呼叫函式之後的回傳結果我們就可以用索引來取得
var score = getScore();
var t1 = score.$1;
更進一步,如果我們要讓程式的可讀性更好可以使用解構
var (homeTeam, homeScore, awayTeam, awayScore) = getScore();
在 Dart 中函式 Function
被視為一種型別就像 String
或 num
型別一樣。表示函式可以被賦值給變數或作為其他函式的參數。
void greet() {
print("Hello, world!");
}
void main() {
Function x = greet;
x();
var y = greet;
String msg = y();
print(msg);
}
匿名函式就是沒有名稱的函式;或稱為 Lambda 或閉包。例如 List
的 forEach()
就是一個很好的例子。我們需要傳入一個函式,列表的每一個元素都會和這個函式搭配執行。
void main() {
List list = [1, 2, 3, 4];
list.forEach((number) => print('Hi, $number'));
}
上面範例我們傳入匿名函式
(number) => print('Hi, $number')
=>
也就是大家熟悉的箭頭函式,該匿名函式有一個參數,然後使用 print
。
Lexical Scoping 詞彙作用域是一種作用域規則,基於程式碼的結構和層次決定一個變數的作用域。也就是一個變數所在的位置會決定誰能存取或被執行。
globals() {
print('Top-level');
}
simple() {
globals() {
print('This is nested function');
}
}
main() {
simple();
globals();
}
通過理解和運用這些控制流程和函式,我們進一步可以理解開發 Flutter 時遇到的那些奇妙語法。下一個章節,我們將開始探討物件導向的部分。